package nl.utwente.fmt.vertalerbouw.tarrh.symboltable;

import java.util.HashMap;
import java.util.Map;
import java.util.Stack;

/**
 * Class containing entries for all variables, functions and classes in their
 * respective scope levels.
 * 
 * @param <VariableEntry>
 *            holds all data related to variable entries.
 * @param <FunctionEntry>
 *            holds all data related to function entries.
 * @param <ClassEntry>
 *            holds all data related to class entries.
 */
public class SymbolTable<VariableEntry, FunctionEntry, ClassEntry> {
	private Map<String, ClassEntry> classTable = new HashMap<String, ClassEntry>();
	private ClassEntry currentClassScope = null;
	private int currentLevel = -1;
	private Map<ClassEntry, Map<String, FunctionEntry>> functionTable = new HashMap<ClassEntry, Map<String, FunctionEntry>>();
	private Stack<Boolean> isClassScopeStack = new Stack<Boolean>();
	private Stack<Map<String, VariableEntry>> variableTable = new Stack<Map<String, VariableEntry>>();

	/**
	 * Closes the current scope. All identifiers in the current scope will be
	 * removed from the SymbolTable.
	 */
	public void closeScope() {
		variableTable.pop();
		isClassScopeStack.pop();
		currentLevel--;
		if (currentLevel == 0) {
			currentClassScope = null;
		}
	}

	/**
	 * Enter a class with the specified identifier and open the respective
	 * scope.
	 * 
	 * @param identifier
	 *            the identifier representing the class.
	 * @param entry
	 *            the class entry to be stored alongside the class.
	 * @throws SymbolTableException
	 *             if the scope level is invalid or the identifier is already
	 *             defined in the current scope.
	 */
	public void enterClassAndOpenScope(String identifier, ClassEntry entry) throws SymbolTableException {
		if (currentLevel == 0) {
			if (!classTable.containsKey(identifier)) {
				classTable.put(identifier, entry);
			} else {
				throw new SymbolTableException("Class identifier '" + identifier
						+ "' already defined in current scope.");
			}
			currentClassScope = entry;
			openScope(true);
		} else {
			throw new SymbolTableException("Invalid scope level for class declaration.");
		}
	}

	/**
	 * Enter a function with the specified identifier and open the respective
	 * scope.
	 * 
	 * @param identifier
	 *            the identifier representing the function.
	 * @param entry
	 *            the function entry to be stored alongside the function.
	 * @throws SymbolTableException
	 *             if the scope level is invalid or the identifier is already
	 *             defined in the current scope.
	 */
	public void enterFunctionAndOpenScope(String identifier, FunctionEntry entry) throws SymbolTableException {
		if (currentLevel == 0 || currentLevel == 1 && currentClassScope != null) {
			Map<String, FunctionEntry> localFunctionTable = functionTable.get(currentClassScope);
			if (localFunctionTable == null) {
				localFunctionTable = new HashMap<String, FunctionEntry>();
				functionTable.put(currentClassScope, localFunctionTable);
			} else if (localFunctionTable.containsKey(identifier)) {
				throw new SymbolTableException("Function identifier '" + identifier
						+ "' already defined in current scope.");
			}
			localFunctionTable.put(identifier, entry);
			openScope();
		} else {
			throw new SymbolTableException("Invalid scope level for function declaration.");
		}
	}

	/**
	 * Enter a variable with the specified identifier.
	 * 
	 * @param identifier
	 *            the identifier representing the variable.
	 * @param entry
	 *            the variable entry to be stored alongside the variable.
	 * @throws SymbolTableException
	 *             if the scope level is invalid or the identifier is already
	 *             defined in the current scope.
	 */
	public void enterVariable(String identifier, VariableEntry entry) throws SymbolTableException {
		if (currentLevel > -1) {
			if (!variableTable.peek().containsKey(identifier)) {
				variableTable.peek().put(identifier, entry);
			} else {
				throw new SymbolTableException("Variable identifier '" + identifier
						+ "' already defined in current scope.");
			}
		} else {
			throw new SymbolTableException("Invalid scope level for variable declaration.");
		}
	}

	/**
	 * Get the current scope level.
	 * 
	 * @return the current scope level.
	 */
	public int getCurrentScopeLevel() {
		return currentLevel;
	}

	/**
	 * Open a new scope.
	 */
	public void openScope() {
		openScope(false);
	}

	/**
	 * Open a new scope with the specified class scope indicator.
	 * 
	 * @param isClassScope
	 *            true if the scope is a class scope.
	 */
	private void openScope(boolean isClassScope) {
		currentLevel++;
		variableTable.push(new HashMap<String, VariableEntry>());
		isClassScopeStack.push(isClassScope);
	}

	/**
	 * Get the class entry corresponding with the specified class identifier.
	 * 
	 * @return class entry corresponding with specified class identifier.
	 */
	public ClassEntry retrieveClass(String identifier) throws SymbolTableException {
		if (classTable.containsKey(identifier)) {
			return classTable.get(identifier);
		}
		throw new SymbolTableException("Class identifier '" + identifier + "' not defined in scope.");
	}

	/**
	 * Get the function entry corresponding with the specified function
	 * identifier. If multiple functions with the same identifier exist, this
	 * will return the entry corresponding to the one defined last.
	 * 
	 * @return function entry corresponding with specified function identifier.
	 */
	public FunctionEntry retrieveFunction(String identifier) throws SymbolTableException {
		if (currentClassScope != null) {
			try {
				return retrieveMemberFunction(currentClassScope, identifier);
			} catch (SymbolTableException e) {
				// If not in the current class scope, check the global function
				// scope.
			}
		}
		Map<String, FunctionEntry> localFunctionTable = functionTable.get(null);
		if (localFunctionTable != null && localFunctionTable.containsKey(identifier)) {
			return localFunctionTable.get(identifier);
		}
		throw new SymbolTableException("Global function identifier '" + identifier + "' not defined in scope.");
	}

	/**
	 * Get the function entry corresponding with the specified function
	 * identifier in the specified class.
	 * 
	 * @return function entry corresponding with specified function identifier
	 *         in the specified class.
	 */
	public FunctionEntry retrieveMemberFunction(ClassEntry classEntry, String identifier) throws SymbolTableException {
		Map<String, FunctionEntry> localFunctionTable = functionTable.get(classEntry);
		if (localFunctionTable != null && localFunctionTable.containsKey(identifier)) {
			return localFunctionTable.get(identifier);
		}
		throw new SymbolTableException("Function identifier '" + identifier + "' in class not defined in scope.");
	}

	/**
	 * Get the variable entry corresponding with the specified variable
	 * identifier. If multiple variables with the same identifier exist, this
	 * will return the entry corresponding to the one defined last.
	 * 
	 * @return variable entry corresponding with specified variable identifier.
	 */
	public VariableEntry retrieveVariable(String identifier) throws SymbolTableException {
		for (int i = currentLevel; i >= 0; i--) {
			if (variableTable.get(i).containsKey(identifier)) {
				return variableTable.get(i).get(identifier);
			}
		}
		throw new SymbolTableException("Variable identifier '" + identifier + "' not defined in scope.");
	}
}
